/*
 * Common usable utilities
 *
 * Copyright (c) 2006
 *   Petr Hadraba <hadrabap@bluetone.cz>
 *
 * Author: Petr Hadraba
 *
 * --
 *
 * XML Utilities
 */

package global.sandbox.xmlutilities;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Iterator;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * This class provides simple XML utilities.
 * 
 * @author Petr Hadraba
 * 
 * @version 1.1
 */
public class XMLUtilities {
    
    /**
     * stores schema factory
     */
    private final SchemaFactory schemaFactory;
    
    /**
     * stores document builder factory
     */
    private final DocumentBuilderFactory documentBuilderFactory;
    
    /**
     * stores document builder
     */
    private final DocumentBuilder documentBuilder;
    
    /**
     * stores localTransformer factory
     */
    private final TransformerFactory transformerFactory;
    
    /**
     * stores localTransformer
     */
    private final Transformer transformer;
    
    /**
     * stores XPath factory
     */
    private final XPathFactory xpathFactory;
    
    /**
     * stores xpath evaluator
     */
    private final XPath xpath;
    
    /**
     * stores the default NamespaceContext
     */
    private final NamespaceContext defaultNamespaceContext;
    
    /**
     * initializes XMLUtilities
     * 
     * Creates new factories and new instanties
     * 
     * @throws ParserConfigurationException
     * @throws TransformerConfigurationException
     * @throws XPathFactoryConfigurationException
     */
    public XMLUtilities() throws ParserConfigurationException,
            TransformerConfigurationException,
            XPathFactoryConfigurationException {
        this(null, null, null, null, null, null, null, null);
    }
    
    /**
     * initializes XMLUtilities
     * 
     * Creates new factories and new instaties according to custom objects.
     * 
     * @param documentBuilderFactory
     *            custom DocumentBuilderFactory
     * @param documentBuilder
     *            custom DocumentBuilder
     * @param schemaFactory
     *            custom SchemaFactory
     * @param transformerFactory
     *            custom TransformerFactory
     * @param transformer
     *            custom Transformer
     * @param xpathFactory
     *            custom XPathFactory
     * @param xpath
     *            custom XPath
     * @param defaultNamespaceContext
     *            custom namespaces
     * 
     * @throws ParserConfigurationException
     * @throws TransformerConfigurationException
     * @throws XPathFactoryConfigurationException
     */
    public XMLUtilities(final DocumentBuilderFactory documentBuilderFactory,
            final DocumentBuilder documentBuilder,
            final SchemaFactory schemaFactory,
            final TransformerFactory transformerFactory,
            final Transformer transformer, final XPathFactory xpathFactory,
            final XPath xpath, final NamespaceContext defaultNamespaceContext)
            throws ParserConfigurationException,
            TransformerConfigurationException,
            XPathFactoryConfigurationException {
        super();
        
        if (documentBuilderFactory == null) {
            this.documentBuilderFactory = DocumentBuilderFactory.newInstance();
            this.documentBuilderFactory.setNamespaceAware(true);
        } else {
            this.documentBuilderFactory = documentBuilderFactory;
        }
        
        if (documentBuilder == null) {
            this.documentBuilder = this.documentBuilderFactory
                    .newDocumentBuilder();
        } else {
            this.documentBuilder = documentBuilder;
        }
        
        if (schemaFactory == null) {
            this.schemaFactory = SchemaFactory
                    .newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        } else {
            this.schemaFactory = schemaFactory;
        }
        
        if (transformerFactory == null) {
            this.transformerFactory = TransformerFactory.newInstance();
        } else {
            this.transformerFactory = transformerFactory;
        }
        
        if (transformer == null) {
            this.transformer = this.transformerFactory.newTransformer();
        } else {
            this.transformer = transformer;
        }
        
        if (xpathFactory == null) {
            this.xpathFactory = XPathFactory
                    .newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI);
        } else {
            this.xpathFactory = xpathFactory;
        }
        
        if (xpath == null) {
            this.xpath = this.xpathFactory.newXPath();
        } else {
            this.xpath = xpath;
        }
        
        if (defaultNamespaceContext == null) {
            this.defaultNamespaceContext = new NamespaceContextImpl();
        } else {
            this.defaultNamespaceContext = defaultNamespaceContext;
        }
    }
    
    /**
     * obtains SchemaFactory object internaly used by the XMLUtilities
     * 
     * @return SchemaFactory object
     */
    public SchemaFactory getSchemaFactory() {
        return schemaFactory;
    }
    
    /**
     * obtains DocumentBuilderFactory object internaly used by the XMLUtilities
     * 
     * @return DocumentBuilderFactory object
     */
    public DocumentBuilderFactory getDocumentBuilderFactory() {
        return documentBuilderFactory;
    }
    
    /**
     * obtains DocumentBuilder object internaly used by the XMLUtilities
     * 
     * @return DocumentBuilder object
     */
    public DocumentBuilder getDocumentBuilder() {
        return documentBuilder;
    }
    
    /**
     * obtains TransformerFactory object internaly used by the XMLUtilities
     * 
     * @return TransformerFactory object
     */
    public TransformerFactory getTransformerFactory() {
        return transformerFactory;
    }
    
    /**
     * obtains Transformer object internaly used by the XMLUtilities
     * 
     * @return Transformer object
     */
    public Transformer getTransformer() {
        return transformer;
    }
    
    /**
     * obtains XPathFactory object internaly used by the XMLUtilities
     * 
     * @return XPathFactory object
     */
    public XPathFactory getXPathFactory() {
        return xpathFactory;
    }
    
    /**
     * obtains XPath object internaly used by the XMLUtilities
     * 
     * @return XPath object
     */
    public XPath getXPath() {
        return xpath;
    }
    
    /**
     * obtains default NamespaceContext used by the XMLUtilities
     *
     * @return default NamespaceContenxt object
     */
    public NamespaceContext getDefaultNamespaceContext() {
        return defaultNamespaceContext;
    }
    
    /**
     * validates XML file using XML schema
     * 
     * @param xmlDocument
     *            document to validate
     * @param xmlSchema
     *            XMLSchema
     * 
     * @return null if successful, string with error otherwise
     * 
     * @throws IOException
     */
    public String validateXmlUsingSchema(final Source xmlDocument,
            final Source xmlSchema) throws IOException {
        final Validator validator;
        
        try {
            validator = schemaFactory.newSchema(xmlSchema).newValidator();
            
            validator.validate(xmlDocument);
        } catch (SAXException e) {
            return e.getMessage();
        }
        
        return null;
    }
    
    /**
     * loads XML from file specified with file name
     * 
     * @param fileName
     *            file to load
     * 
     * @return Document object
     * 
     * @throws SAXException
     * @throws IOException
     * @throws FileNotFoundException
     */
    public Document loadDocumentFromFile(final String fileName)
            throws FileNotFoundException, IOException, SAXException {
        return loadDocumentFromFile(new File(fileName));
    }
    
    /**
     * loads XML from file specified with file name
     * 
     * @param fileName
     *            file to load
     * @param documentBuilder
     *            custom document builder
     * 
     * @return Document object
     * 
     * @throws SAXException
     * @throws IOException
     * @throws FileNotFoundException
     */
    public Document loadDocumentFromFile(final String fileName,
            final DocumentBuilder documentBuilder)
            throws FileNotFoundException, IOException, SAXException {
        return loadDocumentFromFile(new File(fileName), documentBuilder);
    }
    
    /**
     * loads XML from file specified with the File object
     * 
     * @param file
     *            file to load
     * 
     * @return Document object
     * 
     * @throws SAXException
     * @throws IOException
     * @throws FileNotFoundException
     */
    public Document loadDocumentFromFile(final File file)
            throws FileNotFoundException, IOException, SAXException {
        return loadDocumentFromFile(file, documentBuilder);
    }
    
    /**
     * loads XML from file specified with the file object
     * 
     * @param file
     *            file to load
     * @param documentBuilder
     *            custom document builder
     * 
     * @return Document object
     * 
     * @throws FileNotFoundException
     * @throws IOException
     * @throws SAXException
     */
    public Document loadDocumentFromFile(final File file,
            final DocumentBuilder documentBuilder)
            throws FileNotFoundException, IOException, SAXException {
        final InputStream inputStream = new FileInputStream(file);
        
        final Document document;
        try {
            document = loadDocumentFromStream(inputStream, documentBuilder);
        } finally {
            inputStream.close();
        }
        
        return document;
    }
    
    /**
     * loads XML from specified input stream
     * 
     * @param is
     *            input stream
     * 
     * @return Document object
     * 
     * @throws IOException
     * @throws SAXException
     */
    public Document loadDocumentFromStream(final InputStream is)
            throws IOException, SAXException {
        return loadDocumentFromStream(is, documentBuilder);
    }
    
    /**
     * loads XML from specified input stream
     * 
     * @param is
     *            input stream
     * @param documentBuilder
     *            custom Document Builder
     * 
     * @return Document object
     * 
     * @throws IOException
     * @throws SAXException
     */
    public Document loadDocumentFromStream(final InputStream is,
            final DocumentBuilder documentBuilder) throws IOException,
            SAXException {
        final BufferedInputStream bufferedInputStream
                = new BufferedInputStream(is);
        
        final Document document = documentBuilder.parse(bufferedInputStream);
        
        bufferedInputStream.close();
        
        return document;
    }
    
    /**
     * converts XML Document into String
     * 
     * @param source
     *            Document to convert
     * 
     * @return XML in the String
     * 
     * @throws TransformerException
     * @throws IOException
     */
    public String documentToString(final Document source)
            throws TransformerException, IOException {
        return sourceToString(XMLTools.documentToDOMSource(source));
    }
    
    /**
     * converts XML Source into String
     * 
     * @param source
     *            Document to convert
     * 
     * @return XML in the String
     * 
     * @throws TransformerException
     * @throws IOException
     */
    public String sourceToString(final Source source)
            throws TransformerException, IOException {
        final StringWriter stringWriter = new StringWriter();
        final StreamResult streamResult = new StreamResult(stringWriter);
        
        transformer.setParameter("", "");
        transformer.clearParameters();
        //localTransformer.reset();
        
        transformer.transform(source, streamResult);
        
        final String result = stringWriter.toString();
        
        stringWriter.close();
        
        return result;
    }
    
    /**
     * evaluates specified XPath expression on specified contenxt node
     * 
     * @param query
     *            XPath expression
     * @param context
     *            context node
     * 
     * @return NodeList or null if no matches
     * 
     * @throws XPathExpressionException
     */
    public NodeList evaluateXPath(final String query, final Node context)
            throws XPathExpressionException {
        return evaluateXPath(query, context, null);
    }
    
    /**
     * evaluates specified XPath expression on specified contenxt node
     * 
     * @param query
     *            XPath expression
     * @param context
     *            context node
     * @param namespaces
     *            namespace context
     * 
     * @return NodeList or null if no matches
     * 
     * @throws XPathExpressionException
     */
    public NodeList evaluateXPath(final String query, final Node context,
            final NamespaceContext namespaces) throws XPathExpressionException {
        xpath.reset();
        
        if (namespaces == null) {
            xpath.setNamespaceContext(defaultNamespaceContext);
        } else {
            xpath.setNamespaceContext(namespaces);
        }
        
        return (NodeList) xpath
                .evaluate(query, context, XPathConstants.NODESET);
    }
    
    /**
     * evaluates specified XPath expression and returnes first Node if XPath has
     * matches
     * 
     * @param query
     *            XPath expression
     * @param context
     *            context node
     * 
     * @return first node or null
     * 
     * @throws XPathExpressionException
     */
    public Node getFirstNodeForXPath(final String query, final Node context)
            throws XPathExpressionException {
        return getFirstNodeForXPath(query, context, null);
    }
    
    /**
     * evaluates specified XPath expression and returnes first Node if XPath has
     * matches
     * 
     * @param query
     *            XPath expression
     * @param context
     *            context node
     * @param namespaces
     *            namespace context
     * 
     * @return first node or null
     * 
     * @throws XPathExpressionException
     */
    public Node getFirstNodeForXPath(final String query, final Node context,
            NamespaceContext namespaces) throws XPathExpressionException {
        final NodeList nodes = evaluateXPath(query, context, namespaces);
        
        if (nodes == null) {
            return null;
        }
        
        if (nodes.getLength() > 0) {
            return nodes.item(0);
        }
        
        return null;
    }
    
    /**
     * transformes specified XML source using specified XSLT template
     * 
     * @param xsltTemplate
     *            template to use
     * @param document
     *            source XML document
     * @param parameters
     *            parameters to propagate to the localTransformer
     * 
     * @return resulting XML in the <code>String</code>
     * 
     * @throws TransformerConfigurationException
     * @throws TransformerException
     */
    public String transformToString(final Source xsltTemplate,
            final Source document, final Map<String, Object> parameters)
            throws TransformerConfigurationException, TransformerException {
        final Transformer localTransformer = transformerFactory
                .newTransformer(xsltTemplate);
        
        if (parameters != null) {
            final Iterator<String> it = parameters.keySet().iterator();
            
            while (it.hasNext()) {
                final String parameterName = it.next();
                
                localTransformer.setParameter(parameterName, parameters
                        .get(parameterName));
            }
        }
        
        final StringWriter sw = new StringWriter();
        final StreamResult sr = new StreamResult(sw);
        
        localTransformer.transform(document, sr);
        
        final String result = sw.toString();
        
        try {
            sw.close();
        } catch (IOException e) {
            return null;
        }
        
        return result;
    }
    
    /**
     * transforms specified XML source using specified XSLT (compiled) template
     * 
     * @param xsltTemplate
     *            template to use
     * @param document
     *            source XML document to transform
     * @param parameters
     *            parameters to propagate to the localTransformer
     * 
     * @return resulting XML in the <code>String</code>
     * 
     * @throws TransformerException
     */
    public String transformToString(final Templates xsltTemplate,
            final Source document, final Map<String, Object> parameters)
            throws TransformerException {
        final Transformer localTransformer = xsltTemplate.newTransformer();
        
        if (parameters != null) {
            final Iterator<String> it = parameters.keySet().iterator();
            
            while (it.hasNext()) {
                final String parameterName = it.next();
                
                localTransformer.setParameter(parameterName, parameters
                        .get(parameterName));
            }
        }
        
        final StringWriter sw = new StringWriter();
        final StreamResult sr = new StreamResult(sw);
        
        localTransformer.transform(document, sr);
        
        final String result = sw.toString();
        
        try {
            sw.close();
        } catch (IOException e) {
            return null;
        }
        
        return result;
    }
    
    /**
     * transforms specified XML source using specified XSLT (compiled) template
     * 
     * @param xsltTemplate
     *            template to use
     * @param document
     *            source XML document to transform
     * 
     * @return resulting XML in the <code>String</code>
     * 
     * @throws TransformerException
     */
    public String transformToString(final Templates xsltTemplate,
            final Source document) throws TransformerException {
        return transformToString(xsltTemplate, document, null);
    }
    
    /**
     * transformes specified XML document using specified XSLT template
     * 
     * @param xsltTemplate
     *            XSLT template
     * @param document
     *            source XML document
     * 
     * @return resulting XML document in the <code>String</code>
     * 
     * @throws TransformerConfigurationException
     * @throws TransformerException
     */
    public String transformToString(final Source xsltTemplate,
            final Source document) throws TransformerConfigurationException,
            TransformerException {
        return transformToString(xsltTemplate, document, null);
    }
    
    /**
     * transformes specified XML document using specified XSLT template
     * 
     * @param xsltTemplate
     *            XSLT template
     * @param document
     *            source XML document
     * @return resulting XML document in the <code>Document</code>
     * 
     * @throws TransformerConfigurationException
     * @throws XPathFactoryConfigurationException
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException
     */
    public Document transformToDocument(final Source xsltTemplate,
            final Source document) throws TransformerConfigurationException,
            XPathFactoryConfigurationException, ParserConfigurationException,
            SAXException, IOException, TransformerException {
        final XMLUtilities xmlUtils = new XMLUtilities();
        
        return XMLTools.loadDocumentFromString(transformToString(xsltTemplate,
                document, null), xmlUtils);
    }
    
    /**
     * transforms specified XML document using XSLT (compiled) template
     * 
     * @param xsltTemplate
     *            template to use
     * @param document
     *            document to transform
     * 
     * @return resulting XML document in the <code>Document</code>
     * 
     * @throws TransformerConfigurationException
     * @throws XPathFactoryConfigurationException
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException
     */
    public Document transformToDocument(final Templates xsltTemplate,
            final Source document) throws TransformerConfigurationException,
            XPathFactoryConfigurationException, ParserConfigurationException,
            SAXException, IOException, TransformerException {
        final XMLUtilities xmlUtils = new XMLUtilities();
        
        return XMLTools.loadDocumentFromString(transformToString(xsltTemplate,
                document, null), xmlUtils);
    }
    
    /**
     * transformes specified XML document using specified XSLT template
     * 
     * @param xsltTemplate
     *            XSLT template
     * @param document
     *            source XML document
     * @param parameters
     *            parameters for the template
     * @return resulting XML document in the <code>Document</code>
     * 
     * @throws TransformerConfigurationException
     * @throws XPathFactoryConfigurationException
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException
     */
    public Document transformToDocument(final Source xsltTemplate,
            final Source document, final Map<String, Object> parameters)
            throws TransformerConfigurationException,
            XPathFactoryConfigurationException, ParserConfigurationException,
            SAXException, IOException, TransformerException {
        final XMLUtilities xmlUtils = new XMLUtilities();
        
        return XMLTools.loadDocumentFromString(transformToString(xsltTemplate,
                document, parameters), xmlUtils);
    }
    
    /**
     * transforms sepecified XML using XSLT (compiled) template
     * 
     * @param xsltTemplate
     *            template to use
     * @param document
     *            XML document to transform
     * @param parameters
     *            parameters for the transformation
     * 
     * @return resulting XML document in the <code>Document</code>
     * 
     * @throws TransformerConfigurationException
     * @throws XPathFactoryConfigurationException
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TransformerException
     */
    public Document transformToDocument(final Templates xsltTemplate,
            final Source document, final Map<String, Object> parameters)
            throws TransformerConfigurationException,
            XPathFactoryConfigurationException, ParserConfigurationException,
            SAXException, IOException, TransformerException {
        final XMLUtilities xmlUtils = new XMLUtilities();
        
        return XMLTools.loadDocumentFromString(transformToString(xsltTemplate,
                document, parameters), xmlUtils);
    }
    
}
